﻿// The MIT License (MIT)
//
// Copyright (c) 2015 FramTack, LLC
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), 
// to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 
// and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
// DEALINGS IN THE SOFTWARE.
//
// For more information visit the project home page at http://solutionfamily.codeplex.com or the owner's site at http://solution-family.com
//

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Text;

namespace SolutionFamily.IO
{
    public class DiagnosticTroubleCode
    {
    }

    public class PGN65226 : J1939ParameterGroup
    {
        // active diagnostic trouble codes

        // SPN == Suspect Parameter Number
        // FMI = FailureModeIndicator
        // OC = occurrence count
        // CM = conversion method
        // DTC = Diagnostic Trouble Code

        private DiagnosticTroubleCode[] m_dtcs;

        internal PGN65226()
            : base(65226, "DM1", 1000)
        {
        }

        public DiagnosticTroubleCode[] DTCs
        {
            get { return m_dtcs; }
        }

        internal override void PopulateParametersFromData(byte[] data)
        {
            // no error means data[2] to data[6] are 0x00
            if(data[2] == 0)
            {
                m_dtcs = new DiagnosticTroubleCode[0];
                Debug.WriteLine("DM1 No Error");
                return;
            }

            Debug.WriteLine("DM1 Error Code");
            // TODO: decipher these
        }
        
        private int GetSPN(byte[] data, int offset)
        {
            var spn = (data[offset + 2] >> 5) << 16;
            spn |= (data[offset + 1] << 8);
            spn |= data[offset];

            return spn;
        }

        private int GetFMI(byte[] data, int offset)
        {
            // FMI is the 5 LSB of the third byte in
            var fmi = (data[offset + 2] & 0x1F);
            return fmi;
        }

        private bool GetCM(byte[] data, int offset)
        {
            return (data[offset + 3] & 0x80) != 0;
        }

        private int GetOC(byte[] data, int offset)
        {
            return data[offset + 3] & 0x7F;
        }
    }

    public class J1939ParameterGroup
    {
        internal J1939ParameterGroup(int pgn, string name, int transmissionRate)
        {
            Parameters = new J1939ParameterCollection();
            PGN = pgn;
            Name = name;
            TransmissionRate = transmissionRate;
        }

        public int PGN { get; private set; }
        public string Name { get; private set; }
        public int TransmissionRate { get; private set; }

        public J1939ParameterCollection Parameters { get; private set; }

        public override string ToString()
        {
            return string.Format("{0}: {1}", PGN, Name);
        }

        internal virtual void PopulateParametersFromData(byte[] data)
        {
            foreach (var p in Parameters)
            {
                switch (Type.GetTypeCode(p.DataType))
                {
                    case TypeCode.Double:
                        p.Value = DecodeDoubleParameter(data, p.DataOffset, p.DataLength, p.Offset, p.Scale);
                        break;
                    case TypeCode.Int32:
                        p.Value = DecodeIntParameter(data, p.DataOffset, p.DataLength, p.Offset, p.Scale);
                        break;
                    default:
                        throw new NotSupportedException();
                }
            }
        }

        private int DecodeIntParameter(byte[] data, int dataPositionOffset, int dataLength, double offset, double scale)
        {
            switch (dataLength)
            {
                case 1:
                    return (int)(offset + data[dataPositionOffset] * scale);
                case 2:
                    return (int)(offset + BitConverter.ToUInt16(data, dataPositionOffset) * scale);
                case 4:
                    return (int)(offset + BitConverter.ToUInt32(data, dataPositionOffset) * scale);
                default:
                    throw new NotSupportedException();
            }
        }

        private double DecodeDoubleParameter(byte[] data, int dataPositionOffset, int dataLength, double offset, double scale)
        {
            switch (dataLength)
            {
                case 1:
                    return offset + data[dataPositionOffset] * scale;
                case 2:
                    return offset + BitConverter.ToUInt16(data, dataPositionOffset) * scale;
                case 4:
                    return offset + BitConverter.ToUInt32(data, dataPositionOffset) * scale;
                default:
                    throw new NotSupportedException();
            }
        }

        public byte[] Serialize()
        {
            // the binary size is the offset of the last parameter plus it's length
            var size = (from p in Parameters
                        orderby p.DataOffset descending
                        select p.DataOffset + p.DataLength).First();

            var data = new byte[size];

            foreach(var p in Parameters)
            {
                if (p.Value == null)
                {
                }

                switch (Type.GetTypeCode(p.DataType))
                {
                    case TypeCode.Double:
                        EncodeDoubleParameter(data, p);
                        break;
                    case TypeCode.Int32:
                    case TypeCode.UInt32:
                        EncodeIntParameter(data, p);
                        break;
                    default:
                        throw new NotSupportedException();
                }
            }

            return data;
        }

        private void EncodeIntParameter(byte[] destination, J1939Parameter parameter)
        {
            switch (parameter.DataLength)
            {
                case 1:
                    try
                    {
                        destination[parameter.DataOffset] = (byte)((Convert.ToInt32(parameter.Value) - parameter.Offset) / parameter.Scale);
                    }
                    catch (FormatException)
                    {
                        throw new Exception(string.Format("Cannot convert '{0}' to a 1-byte integer", parameter.Value));
                    }
                    break;
                case 2:
                    try
                    {
                        var val16 = (ushort)((Convert.ToInt32(parameter.Value) - parameter.Offset) / parameter.Scale);
                        Buffer.BlockCopy(BitConverter.GetBytes(val16), 0, destination, parameter.DataOffset, parameter.DataLength);
                    }
                    catch (FormatException)
                    {
                        throw new Exception(string.Format("Cannot convert '{0}' to a 2-byte integer", parameter.Value));
                    }
                    break;
                case 4:
                    try
                    {
                        var val32 = (uint)((Convert.ToInt32(parameter.Value) - parameter.Offset) / parameter.Scale);
                        Buffer.BlockCopy(BitConverter.GetBytes(val32), 0, destination, parameter.DataOffset, parameter.DataLength);
                    }
                    catch (FormatException)
                    {
                        throw new Exception(string.Format("Cannot convert '{0}' to a 4-byte integer", parameter.Value));
                    }
                    break;
                default:
                    throw new NotSupportedException();
            }
        }

        private void EncodeDoubleParameter(byte[] destination, J1939Parameter parameter)
        {
            switch (parameter.DataLength)
            {
                case 1:
                    destination[parameter.DataOffset] = (byte)((Convert.ToDouble(parameter.Value) - parameter.Offset) / parameter.Scale);
                    break;
                case 2:
                    var val16 = (ushort)((Convert.ToDouble(parameter.Value) - parameter.Offset) / parameter.Scale);
                    Buffer.BlockCopy(BitConverter.GetBytes(val16), 0, destination, parameter.DataOffset, parameter.DataLength);
                    break;
                case 4:
                    var val32 = (uint)((Convert.ToDouble(parameter.Value) - parameter.Offset) / parameter.Scale);
                    Buffer.BlockCopy(BitConverter.GetBytes(val32), 0, destination, parameter.DataOffset, parameter.DataLength);
                    break;
                default:
                    throw new NotSupportedException();
            }
        }

        public J1939ParameterGroup Copy()
        {
            var pg = new J1939ParameterGroup(this.PGN, this.Name, this.TransmissionRate);

            foreach (var p in this.Parameters)
            {
                pg.Parameters.Add((J1939Parameter)p.Copy());
            }

            return pg;
        }
    }
}
